package com.timk.goserver.server.model;

import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

import org.hibernate.Session;

import com.timk.goserver.client.model.ClientGameInfo;
import com.timk.goserver.client.model.ClientUserInfo;
import com.timk.goserver.client.model.GameInvite;
import com.timk.goserver.client.model.GamePlayerInfo;
import com.timk.goserver.client.services.GameService;
import com.timk.goserver.server.util.GSLogger;


/**
 * A game record.
 * @author TKington
 *
 */
public class ServerGameInfo { // NOPMD - Complexity
	private int gameId;
	private int size;
	private List playerInfo;
	private String nextToPlay;
	private int handicap;
	private double komi;
	private boolean rated;
	private String sgf;
	private int state;
	private String result;
	private String deadStones;
	private int moveNumber;
	private Date lastMoveTime;
	
	/**
	 * Creates a ServerGameInfo.  Only used for serialization.
	 *
	 */
	public ServerGameInfo() { /* */ }
	
	/**
	 * Creates a ServerGameInfo from an invite
	 * @param invite the GameInvite
	 * @param hbSession the active session
	 */
	public ServerGameInfo(GameInvite invite, Session hbSession) {
		this.size = invite.getSize();
		this.rated = invite.isRated();
		this.state = GameService.GAME_STATE_RUNNING;
		this.result = "";
		this.lastMoveTime = new Date();

		List names = invite.getPlayerNames();
		playerInfo = new ArrayList();
		for(int i = 0; i < names.size(); i++) {
			GamePlayerInfo player = new GamePlayerInfo((String)names.get(i));
			playerInfo.add(player);
		}
		
		ServerUserInfo [] userInfo = getUserInfo(hbSession);

		if(invite.isProperHandicap()) {
			setProperHandicap(userInfo);
			names = getPlayerNames();
		} else {
			this.handicap = invite.getHandicap();
			this.komi = invite.getKomi();
		}
		
		if(handicap < 2) {
			nextToPlay = (String)names.get(0);
		} else {
			nextToPlay = (String)names.get(1);
		}
		
		generateInitialSgf(userInfo);
		moveNumber = -1;
		if(handicap > 1) {
			moveNumber += handicap;
		}
	}
	
	private ServerUserInfo [] getUserInfo(Session hbSession) {
		ServerUserInfo [] info = new ServerUserInfo[playerInfo.size()];
		for(int i = 0; i < info.length; i++) {
			String name = ((GamePlayerInfo)playerInfo.get(i)).getName();
			info[i] = ServerUserInfo.findUser(name, hbSession);
		}
		
		return info;
	}
	
	private void setProperHandicap(ServerUserInfo [] userInfo) {
		int diff = userInfo[1].getEloRating() - userInfo[0].getEloRating();
		
		if(diff < 0) {
			//	Swap black and white
			GamePlayerInfo temp = (GamePlayerInfo)playerInfo.get(0);
			playerInfo.set(0, playerInfo.get(1));
			playerInfo.set(1, temp);
			
			ServerUserInfo user = userInfo[0];
			userInfo[0] = userInfo[1];
			userInfo[1] = user;
			
			diff *= -1;
		}
		
		if(diff > 0 && diff % 100 == 0) {
			//	When diff is divisible by 100, there are two handicaps that could
			//	be used - diff / 100 with -5.5 komi, or one higher with 6.5 komi.
			//	This special case is here so that players with a rank difference of
			//	exactly n stones get an n stone handicap when using proper handicap.
			handicap = diff / 100;
			komi = -5.5;
		} else {
			handicap = diff / 100 + 1;
			int lostkomi = (int)(12.0 * ((diff % 100.0) / 100));
			komi = 6.5 - lostkomi;
		}
		
		if(handicap == 1) {
			handicap = 0;
		} else if(handicap > 9) {
			handicap = 9;
			komi = 0.5;
		}
	}
	
	/**
	 * Creates a ClientGameInfo from this object
	 * @param hbSession the active session
	 * @param curUser the current user
	 * @param includeSgf true to include the Sgf for the game
	 * @return the ClientGameInfo
	 */
	public ClientGameInfo toClientGameInfo(Session hbSession,
			String curUser, boolean includeSgf) {
		ClientGameInfo game = new ClientGameInfo();
		
		game.setGameId(gameId);
		game.setSize(size);
		
		ServerUserInfo [] serverInfo = getUserInfo(hbSession);
		ClientUserInfo [] clientInfo = new ClientUserInfo[serverInfo.length];
		for(int i = 0; i < clientInfo.length; i++) {
			clientInfo[i] = serverInfo[i].toClientUserInfo();
		}
		game.setPlayers(clientInfo);
		
		game.setNextToMove(nextToPlay);
		game.setHandicap(handicap);
		game.setKomi(komi);
		game.setRated(rated);
		
		if(includeSgf) {
			game.setSgf(getStrippedSgf());
		}
		
		game.setDeadStones(deadStones);
		game.setResult(result);
		game.setMoveNumber(moveNumber);
		game.setLastMoveTime(new Date(lastMoveTime.getTime()));
		
		int index = getPlayerIndex(curUser);
		if(index != -1) {
			GamePlayerInfo info = (GamePlayerInfo)playerInfo.get(index);
			game.setNotes(info.getNotes());
			game.setLastInteractiveMove(info.getLastInteractiveMove());
		}
		
		return game;
	}
	
	/**
	 * Returns the index of the player who is next to play
	 * @return the index of the player who is next to play
	 */
	public int getNextPlayerIndex() {
		return getPlayerIndex(nextToPlay);
	}
	
	/**
	 * Gets the index of a player by name.
	 * @param player
	 * @return the index, or -1 if not found
	 */
	public int getPlayerIndex(String player) {
		for(int i = 0; i < playerInfo.size(); i++) {
			GamePlayerInfo info = (GamePlayerInfo)playerInfo.get(i);
			if(player.equals(info.getName())) {
				return i;
			}
		}
		
		return -1;
	}
	
	/**
	 * Sets the notes for a player
	 * @param player the name of the player
	 * @param newNotes the notes
	 */
	public void setNotes(String player, String newNotes) {
		int index = getPlayerIndex(player);
		if(index != -1) {
			GamePlayerInfo info = (GamePlayerInfo)playerInfo.get(index);
			info.setNotes(newNotes);
		}
	}
	
	/**
	 * Gets the names of the players taking black, separated by spaces
	 * @param buf
	 */
	public final void getBlackNames(StringBuffer buf) {
		for(int i = 0; i < playerInfo.size(); i += 2) {
			if(i > 0) {
				buf.append(' ');
			}

			GamePlayerInfo info = (GamePlayerInfo)playerInfo.get(i);
			buf.append(info.getName());
		}
	}
	
	/**
	 * Gets the names of the players taking white, separated by spaces
	 * @param buf
	 */
	public final void getWhiteNames(StringBuffer buf) {
		for(int i = 1; i < playerInfo.size(); i += 2) {
			if(i > 1) {
				buf.append(' ');
			}

			GamePlayerInfo info = (GamePlayerInfo)playerInfo.get(i);
			buf.append(info.getName());
		}
	}
	
	private void generateInitialSgf(ServerUserInfo [] userInfo) { // NOPMD - Complexity
		StringBuffer buf = new StringBuffer(200);
		buf.append("(;GM[1]FF[4]SZ[").append(size);
		buf.append("]PC[igoserver]PB[");
		getBlackNames(buf);
		buf.append("]PW[");
		getWhiteNames(buf);
		buf.append(']');
		
		if(userInfo.length == 2) {
			buf.append("BR[").append(userInfo[0].getRank());
			buf.append("]WR[").append(userInfo[1].getRank()).append(']');
		}
		
		buf.append("KM[").append(komi).append(']');
		
		if(handicap > 1) {
			switch(size) { // NOPMD - Many statements per case
			case 9:
				switch(handicap) {
				case 2: buf.append(";B[gc];B[cg]"); break;
				case 3: buf.append(";B[cg];B[gc];B[cc]"); break;
				case 4: buf.append(";B[cc];B[gc];B[cg];B[gg]"); break;
				case 5: buf.append(";B[cc];B[gc];B[cg];B[gg];B[ee]"); break;
				default:
					GSLogger.logError("Unknown handicap setting");
					break;
				}
				break;
			case 13:
				switch(handicap) {
				case 2: buf.append(";B[jd];B[dj]"); break;
				case 3: buf.append(";B[jd];B[dj];B[dd]"); break;
				case 4: buf.append(";B[jd];B[dj];B[dd];B[jj]"); break;
				case 5: buf.append(";B[jd];B[dj];B[dd];B[jj];B[gg]"); break;
				case 6: buf.append(";B[jd];B[dj];B[dd];B[jj];B[dg];B[jg]"); break;
				case 7: buf.append(";B[jd];B[dj];B[dd];B[jj];B[dg];B[jg];B[gg]"); break;
				case 8: buf.append(";B[jd];B[dj];B[dd];B[jj];B[dg];B[jg];B[gd];B[gj]"); break;
				case 9: buf.append(";B[jd];B[dj];B[dd];B[jj];B[dg];B[jg];B[gd];B[gj];B[gg]"); break;
				default:
					GSLogger.logError("Unknown handicap setting");
					break;
				}
				break;
			case 19:
				switch(handicap) {
				case 2: buf.append(";B[pd];B[dp]"); break;
				case 3: buf.append(";B[pd];B[dp];B[dd]"); break;
				case 4: buf.append(";B[pd];B[dp];B[dd];B[pp]"); break;
				case 5: buf.append(";B[pd];B[dp];B[dd];B[pp];B[jj]"); break;
				case 6: buf.append(";B[pd];B[dp];B[dd];B[pp];B[dj];B[pj]"); break;
				case 7: buf.append(";B[pd];B[dp];B[dd];B[pp];B[dj];B[pj];B[jj]"); break;
				case 8: buf.append(";B[pd];B[dp];B[dd];B[pp];B[dj];B[pj];B[jd];B[jp]"); break;
				case 9: buf.append(";B[pd];B[dp];B[dd];B[pp];B[dj];B[pj];B[jd];B[jp];B[jj]"); break;
				default:
					GSLogger.logError("Unknown handicap setting");
					break;
				}
				break;
			}
		}
		
		buf.append(')');
		
		sgf = buf.toString();
	}
	
	/**
	 * Returns the number of players
	 * @return the number of players
	 */
	public int getNumPlayers() { return playerInfo.size(); }
	
	/**
	 * Returns the players' names
	 * @return List&lt;String&gt;
	 */
	public final List getPlayerNames() {
		List names = new ArrayList();
		for (Iterator iter = playerInfo.iterator(); iter.hasNext();) {
			GamePlayerInfo info = (GamePlayerInfo) iter.next();
			names.add(info.getName());
		}
		
		return names;
	}
	
	/**
	 * Returns the future moves for all players
	 * @return List&lt;String&gt;
	 */
	public List getFutureMoves() {
		List futureMoves = new ArrayList();
		for (Iterator iter = playerInfo.iterator(); iter.hasNext();) {
			GamePlayerInfo info = (GamePlayerInfo) iter.next();
			futureMoves.add(info.getFutureMoves());
		}
		
		return futureMoves;
	}
	
	/**
	 * Sets the future moves for all players
	 * @param futureMoves List&lt;String&gt;
	 */
	public void setFutureMoves(List futureMoves) {
		for(int i = 0; i < playerInfo.size(); i++) {
			GamePlayerInfo info = (GamePlayerInfo)playerInfo.get(i);
			info.setFutureMoves((String)futureMoves.get(i));
		}
	}
	
	/**
	 * Sets the last interactive move for the given player
	 * @param playerIndex the player index
	 * @param lastInteractiveMove the move number
	 */
	public void setLastInteractiveMove(int playerIndex, int lastInteractiveMove) {
		GamePlayerInfo info = (GamePlayerInfo)playerInfo.get(playerIndex);
		info.setLastInteractiveMove(lastInteractiveMove);
	}
	
	/**
	 * Returns the SGF without comment that the user shouldn't see.
	 * @return the SGF without comment that the user shouldn't see
	 */
	public String getStrippedSgf() {
		if(!"".equals(result)) {
			return sgf;
		}
		
		return removeHiddenComments(sgf);
	}
	
	private String removeHiddenComments(String str) {
		int index = 0;
		String s = str;
		
		while((index = s.indexOf("<h>", index)) != -1) {	// NOPMD - assign operand
			int index2 = s.indexOf("</h>", index);
			if(index2 == -1) {
				break;
			}
			
			s = s.substring(0, index) + s.substring(index2 + 4);
		}
		
		return s;
	}
	
	/**
	 * Removes &lt;h&gt; tags to reveal hidden comments
	 *
	 */
	public void exposeHiddenComments() {
		sgf = sgf.replaceAll("<h>", "");
		sgf = sgf.replaceAll("</h>", "");
	}
	
	/**
	 * Returns the size of the board
	 * @return the size of the board
	 */
	public int getSize() { return size; }
	
	/**
	 * Sets the board size
	 * @param size
	 */
	public void setSize(int size) { this.size = size; }
	
	/**
	 * Returns the game id
	 * @return the game id
	 */
	public int getGameId() { return gameId; }
	
	/**
	 * Sets the game id
	 * @param gameId
	 */
	public void setGameId(int gameId) { this.gameId = gameId; }
	
	/**
	 * Returns the player-specific information
	 * @return List&lt;GamePlayerInfo&gt;
	 */
	List getPlayerInfo() { return playerInfo; }
	
	/**
	 * Sets the player-specific information
	 * @param playerInfo List&lt;GamePlayerInfo&gt;
	 */
	void setPlayerInfo(List playerInfo) { this.playerInfo = playerInfo; }
	
	/**
	 * Returns the next player to move
	 * @return the next player to move
	 */
	public String getNextToPlay() { return nextToPlay; }
	
	/**
	 * Sets the next player to move
	 * @param nextToPlay
	 */
	void setNextToPlay(String nextToPlay) { this.nextToPlay = nextToPlay; }
	
	/**
	 * Sets the next player to move by index
	 * @param playerIndex index of next player
	 */
	public void setNextToPlay(int playerIndex) {
		GamePlayerInfo info = (GamePlayerInfo)playerInfo.get(playerIndex);
		nextToPlay = info.getName();
	}
	
	/**
	 * Clears nextToPlay
	 *
	 */
	public void clearNextToPlay() { nextToPlay = ""; }
	
	/**
	 * Returns the handicap
	 * @return the handicap
	 */
	public int getHandicap() { return handicap; }
	
	/**
	 * Sets the handicap
	 * @param handicap
	 */
	public void setHandicap(int handicap) { this.handicap = handicap; }
	
	/**
	 * Returns the komi
	 * @return the komi
	 */
	public double getKomi() { return komi; }
	
	/**
	 * Sets the komi
	 * @param komi
	 */
	public void setKomi(double komi) { this.komi = komi; }

	/**
	 * Returns the sgf
	 * @return the sgf
	 */
	public String getSgf() {
		return sgf;
	}

	/**
	 * Sets the sgf
	 * @param sgf
	 */
	public void setSgf(String sgf) {
		this.sgf = sgf;
	}

	/**
	 * Returns the game state
	 * @return the game state
	 */
	public int getState() {
		return state;
	}

	/**
	 * Sets the game state
	 * @param state
	 */
	public void setState(int state) {
		this.state = state;
	}

	/**
	 * Returns the game result
	 * @return the game result
	 */
	public String getResult() {
		return result;
	}

	/**
	 * Sets the game result
	 * @param result
	 */
	public void setResult(String result) {
		this.result = result;
	}

	/**
	 * Returns the dead stones
	 * @return the dead stones
	 */
	public String getDeadStones() {
		return deadStones;
	}

	/**
	 * Sets the dead stones
	 * @param deadStones
	 */
	public void setDeadStones(String deadStones) {
		this.deadStones = deadStones;
	}

	/**
	 * Returns true if rated
	 * @return true if rated
	 */
	public boolean isRated() {
		return rated;
	}

	/**
	 * Sets whether the game is rated
	 * @param rated
	 */
	public void setRated(boolean rated) {
		this.rated = rated;
	}

	/**
	 * Returns the number of moves played
	 * @return the number of moves played
	 */
	public int getMoveNumber() {
		return moveNumber;
	}

	/**
	 * Sets the number of moves played
	 * @param moveNumber
	 */
	public void setMoveNumber(int moveNumber) {
		this.moveNumber = moveNumber;
	}

	/**
	 * Returns the time of the last move
	 * @return the time of the last move
	 */
	public Date getLastMoveTime() {
		return lastMoveTime;
	}

	/**
	 * Sets the time of the last move
	 * @param lastMoveTime
	 */
	public void setLastMoveTime(Date lastMoveTime) {
		this.lastMoveTime = lastMoveTime;
	}
}
